package com.brayan.webapp.controller.realestate;

import java.io.Serializable;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.ejb.Asynchronous;
import javax.enterprise.context.SessionScoped;
import javax.faces.application.FacesMessage;
import javax.faces.bean.ManagedBean;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.event.ValueChangeEvent;
import javax.faces.model.SelectItem;
import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;

import org.apache.log4j.Logger;
import org.joda.time.LocalDate;

import com.brayan.webapp.controller.general.i18n.I18N;
import com.brayan.webapp.model.general.country.Country;
import com.brayan.webapp.model.general.geoinfo.GeoInfo;
import com.brayan.webapp.model.realestate.Costs;
import com.brayan.webapp.model.realestate.Reservation;
import com.brayan.webapp.model.realestate.ReservationState;
import com.brayan.webapp.model.realestate.Tenant;
import com.brayan.webapp.model.usermgmt.User;
import com.brayan.webapp.repository.general.country.CountryRepository;
import com.brayan.webapp.repository.realestate.ReservationRepository;
import com.brayan.webapp.repository.usermgmt.TenantRepository;
import com.brayan.webapp.service.auxilliary.MailBean;
import com.brayan.webapp.service.general.geoinfo.GeoIPBean;
import com.brayan.webapp.service.realestate.PriceModelBean;
import com.brayan.webapp.service.realestate.ReservationBean;

@ManagedBean
@SessionScoped
public class ReservationController implements Serializable {

	public ReservationController() {
	}

	private static Logger LOGGER = Logger.getLogger(ReservationController.class);
	private static final long serialVersionUID = -877853351738855552L;

	@Inject
	private TenantRepository tenantRepoService;
	@Inject
	ReservationRepository reservationRepoService;
	@Inject
	private I18N i18n;
	@Inject
	private MailBean mailService;
	@Inject
	private PriceModelBean priceModelService;

	// / SERVICE BEANS ///
	@Inject
	private ReservationBean reservationService;
	@Inject
	private GeoIPBean geoIpService;
	@Inject
	private CountryRepository countryRepo;

	@Inject
	@Named("facesContext")
	private FacesContext facesContext;

	private static final String CALENDAR_PAGE = "calendar";
	private static final String RESERVATION_PAGE = "reservation";

	private boolean doPolling = true;

	@NotNull(message = "Please select a valid start date.")
	private String startDateString;
	@NotNull(message = "Please select a valid end date.")
	private String endDateString;
	@NotNull(message = "Please select the number of guests.")
	private Integer numberOfGuests;
	private Date startDate;
	private Date endDate;
	private String ipAddress;
	private String ipResolvedCountry;
	private String ipResolvedCity;

	private Tenant existingTenant = new Tenant();

	private String givenName;
	private String familyName;
	@NotNull(message = "Please select your sex.")
	private String sex;

	private String street;
	private String additionalAddressInfo;
	private String postalCode;
	private String city;
	private String countryCode;

	private String preferedCurrency;

	private String email;
	private String mobile;
	private String comments;

	List<Reservation> ongoingReservations = new ArrayList<Reservation>();

	// / ENTITIES ///
	private Long existingReservationId;
	private Reservation reservation = new Reservation();
	private boolean hasExistingReservation;
	
	private GeoInfo geoInfo = new GeoInfo();

	private List<SelectItem> countryOptions = new ArrayList<SelectItem>();

	@PostConstruct
	private void loadUserData() {
		User user = (User) facesContext.getExternalContext().getSessionMap().get("user");
		if (user != null) {
			// Check if there is a tenant for this user for additional data.
			Tenant tenant = tenantRepoService.findByEmail(user.getUsername());
			if (tenant != null) {
				existingTenant = tenant;
				populateFields(existingTenant);
			} else {
				populateFieldsByUser(user);
			}
		}
	}

	private User getSessionUser() {
		User user = (User) facesContext.getExternalContext().getSessionMap().get("user");
		return user;
	}

	public void loadExistingReservationById() {
		// Load a reservation, if id exists and is numeric
		setExistingReservationId(getParamIdFromUrl());
		setReservation(reservationRepoService.findReservationById(getParamIdFromUrl()));
		if (getReservation() == null)
			hasExistingReservation = false;
		else
			hasExistingReservation = true;
	}

	private Long getParamIdFromUrl() {
		FacesContext context = FacesContext.getCurrentInstance();
		ExternalContext extContext = context.getExternalContext();
		Map<String, String> params = extContext.getRequestParameterMap();
		int id = Integer.parseInt(params.get("id"));
		return (long) id;
	}

	public boolean hasExistingReservation() {
		return hasExistingReservation;
	}

	public boolean isDoPolling() {
		return doPolling;
	}

	public void setDoPolling(boolean doPolling) {
		LOGGER.debug("Polling of reservation fields for update set to " + doPolling + ".");
		this.doPolling = doPolling;
	}

	public String getStartDateString() {
		if (startDateString != null && !startDateString.isEmpty()) {
			return this.startDateString;
		} else {
			if (getStartDate() != null) {
				DateFormat df = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
				return df.format(getStartDate());
			} else
				return null;
		}
	}

	public void setStartDateString(String startDateString) {
		this.startDateString = startDateString;
		DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
		Date date = null;
		try {
			date = df.parse(startDateString);
			Calendar c = Calendar.getInstance();
			c.setTime(date);
			c.add(Calendar.DATE, 1);
			date = c.getTime();
		} catch (ParseException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		this.setStartDate(date);
	}

	public String getEndDateString() {
		if (endDateString != null && !endDateString.isEmpty()) {
			return this.endDateString;
		} else {
			if (getEndDate() != null) {
				DateFormat df = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
				return df.format(getEndDate());
			} else {
				return null;
			}
		}
	}

	public void setEndDateString(String endDateString) {
		this.endDateString = endDateString;
		DateFormat df = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
		Date date = null;
		try {
			date = df.parse(endDateString);
			Calendar c = Calendar.getInstance();
			c.setTime(date);
			c.add(Calendar.DATE, 1);
			date = c.getTime();
		} catch (ParseException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		this.setEndDate(date);
	}

	public void setStartDate(Date startDate) {
		this.startDate = startDate;
	}

	public Date getStartDate() {
		return startDate;
	}

	public Date getEndDate() {
		return endDate;
	}

	public void setEndDate(Date endDate) {
		this.endDate = endDate;
	}

	// public Date getStartDateMidnight() {
	// return startDate.toDateTimeAtStartOfDay().toDate();
	// }
	//
	// public Date getEndDateMidnight() {
	// return endDate.toDateTimeAtCurrentTime().toDate();
	// }

	public void setIpAddress(String ipAddress) {
		this.ipAddress = ipAddress;
	}

	public Integer getNumberOfGuests() {
		return numberOfGuests;
	}

	public void setNumberOfGuests(Integer numberOfGuests) {
		this.numberOfGuests = numberOfGuests;
	}

	public Tenant getExistingTenant() {
		return existingTenant;
	}

	public void setExistingTenant(Tenant existingTenant) {
		this.existingTenant = existingTenant;
	}

	public String getGivenName() {
		return givenName;
	}

	public void setGivenName(String givenName) {
		this.givenName = givenName;
	}

	public String getFamilyName() {
		return familyName;
	}

	public void setFamilyName(String familyName) {
		this.familyName = familyName;
	}

	public String getSex() {
		return sex;
	}

	public void setSex(String sex) {
		this.sex = sex;
	}

	public String getStreet() {
		return street;
	}

	public void setStreet(String street) {
		this.street = street;
	}

	public String getAdditionalAddressInfo() {
		return additionalAddressInfo;
	}

	public void setAdditionalAddressInfo(String additionalAddressInfo) {
		this.additionalAddressInfo = additionalAddressInfo;
	}

	public String getPostalCode() {
		return postalCode;
	}

	public void setPostalCode(String postalCode) {
		this.postalCode = postalCode;
	}

	public String getCity() {
		return this.city;
	}

	public void setCity(String city) {
		this.city = city;
	}

	public String getCountryCode() {
		if (countryCode == null || countryCode == "") {
			// Deduct from the IP Address.
			asyncSetGeoValuesByIp();
		}
		return countryCode;
	}

	public void setCountryCode(String countryCode) {
		this.countryCode = countryCode;
	}

	public String getPreferedCurrency() {
		return preferedCurrency;
	}

	public void setPreferedCurrency(String preferedCurrency) {
		this.preferedCurrency = preferedCurrency;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		LOGGER.info("Settings email: " + email);
		// aif (mailService.isValidEmailAddress(email))
		this.email = email;
		// else
		// throw new IllegalArgumentException("Passed email (" + email +
		// ") is wrong.");

	}

	public boolean hasValidTenant() {
		if (getExistingTenant() != null)
			return true;
		else
			return false;
	}

	public void emailChangeListener(ValueChangeEvent event) {
		String email = (String) event.getNewValue();
		if (email != null && mailService.isValidEmailAddress(email)) {
			// get data from DB, if available
			LOGGER.info("change listener " + email);
			setEmail(email);
			Tenant tenant = tenantRepoService.findByEmail(email);
			populateFields(tenant);
		}
	}

	private void populateFields(Tenant tenant) {
		if (tenant != null) {
			populateFieldsByUser(tenant);

			setStreet(tenant.getStreetAndNumber());
			setPostalCode(tenant.getPostalCode());
			setCity(tenant.getCity());
			setCountryCode(tenant.getCountryCode());
			setDoPolling(false);
			setExistingTenant(tenant);
		} else {
			setExistingTenant(null);
		}
	}

	private void populateFieldsByUser(User user) {
		setEmail(user.getEmail());
		setMobile(user.getMobile());
		if (user.getSex() != null)
			setSex(user.getSex().toString());
		setGivenName(user.getGivenname());
		setFamilyName(user.getSurname());
	}

	public String getMobile() {
		return mobile;
	}

	public void setMobile(String mobile) {
		this.mobile = mobile;
	}

	public String getComments() {
		return comments;
	}

	public void setComments(String comments) {
		this.comments = comments;
	}

	public Reservation getReservation() {
		return reservation;
	}

	public void setReservation(Reservation reservation) {
		this.reservation = reservation;
	}

	/**
	 * TODO needs to be translated! A printable localized date to push to HTML
	 * gui.
	 * 
	 * @return
	 */
	public String getLocalizedDate() {
		SimpleDateFormat sdfMonth = new SimpleDateFormat("MMMM");
		LocalDate startDate = LocalDate.fromDateFields(getStartDate());
		LocalDate endDate = LocalDate.fromDateFields(getEndDate());

		StringBuilder dateFormat = new StringBuilder(startDate.getDayOfMonth() + ". ");

		boolean isSameMonth = startDate.getMonthOfYear() == endDate.getMonthOfYear();

		if (!isSameMonth)
			dateFormat.append(sdfMonth.format(startDate.toDate()) + " ");
		if (startDate.getYear() != endDate.getYear())
			dateFormat.append(startDate.year() + " ");

		dateFormat.append("bis ");
		if (!isSameMonth)
			dateFormat.append("<span style='padding-left: 80px;'>");
		dateFormat.append(endDate.getDayOfMonth() + ". ");
		dateFormat.append(sdfMonth.format(endDate.toDate()) + " ");
		dateFormat.append(startDate.getYear());
		if (!isSameMonth)
			dateFormat.append("</span>");

		return dateFormat.toString();
	}

	/**
	 * This method returns the correct page name:<br/>
	 * - if no
	 * 
	 * @return
	 */
	public String requestReservationPage() {
		if (!isValidDateString(startDateString) || !isValidDateString(endDateString)) {
			facesContext.addMessage(null, new FacesMessage(i18n.getTrans("ThisIsNotAValidDate")));
			return CALENDAR_PAGE;
		} else
			return RESERVATION_PAGE;
	}

	private boolean isValidDateString(String dateStr) {
		if (dateStr == null)
			return false;
		try {
			LocalDate tmpDate = LocalDate.parse(dateStr);
		} catch (Exception ex) {
			return false;
		}
		return true;
	}

	/**
	 * Save a reservation request. Add meta request information like IP address.
	 */
	public String saveReservationRequest() {

		String returnPage = "calendar.xhtml?faces-redirect=true&anchor=" + getStartDateString();

		if (reservation == null)
			reservation = new Reservation();
		// Set reservation info.
		reservation.setStartDate(getStartDate());
		reservation.setEndDate(getEndDate());
		reservation.setNumberOfGuests(getNumberOfGuests());
		// Set tenant info.

		// TODO move user info to Tenant info.
		Tenant tenant = (Tenant) getSessionUser();

		reservation.setTenant(tenant);

		try {
			reservationService.saveReservation(reservation, getIpAddress());
			// If successful, cleanup the session.
			cleanupSession();
		} catch (Exception ex) {
			LOGGER.error("The Managed Bean failed to save a reservation.", ex);
			facesContext.addMessage(null, new FacesMessage(i18n.getTrans("FailedToSaveYourReservation")));
			returnPage += "&success=false";
		}

		facesContext.addMessage(null, new FacesMessage(i18n.getTrans("YourReservationHasBeenSavedSuccessfully")));
		return returnPage;
	}

	/**
	 * Invalidate the Session. Removes reservation hints.
	 */
	private void cleanupSession() {
		// FacesContext.getCurrentInstance().getExternalContext().invalidateSession();
	}

	public String getIpAddress() {
		HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext()
				.getRequest();
		String ipAddress = request.getHeader("X-FORWARDED-FOR");
		if (ipAddress == null)
			ipAddress = request.getRemoteAddr();
		else
			setIpAddress(ipAddress);

		return ipAddress;
	}

	public String getIpResolvedCountry() {
		return ipResolvedCountry;
	}

	public void setIpResolvedCountry(String ipResolvedCountry) {
		this.ipResolvedCountry = ipResolvedCountry;
	}

	public String getIpResolvedCity() {
		return ipResolvedCity;
	}

	public void setIpResolvedCity(String ipResolvedCity) {
		this.ipResolvedCity = ipResolvedCity;
	}

	public double calculateTotalCosts() {
		Costs costs = reservationService.calculateCosts(getNumberOfGuests(), getStartDate(), getEndDate());
		return costs.getTotalCosts();
	}

	/**
	 * Sort the country options according to priority and seperated.
	 * 
	 * @return
	 */
	public List<SelectItem> getCountryOptions() {
		countryOptions.clear();
		List<Country> countryList = countryRepo.getAllCountriesOrderedByPriority();
		List<SelectItem> selectItemList = new ArrayList<SelectItem>();
		int prioGroup = -1;
		for (Country country : countryList) {
			if (country.getPriorityGroup() != prioGroup) {
				SelectItem empty = new SelectItem("-------------");
				selectItemList.add(empty);
				prioGroup = country.getPriorityGroup();
			}
			SelectItem si = new SelectItem(country.getCountryCode(), country.getCountryName());
			selectItemList.add(si);
		}
		setCountryOptions(selectItemList);

		return countryOptions;
	}

	public void setCountryOptions(List<SelectItem> countryOptions) {
		this.countryOptions = countryOptions;
	}

	/**
	 * Fetches the
	 */
	@Asynchronous
	public void asyncSetGeoValuesByIp() {
		String ipAddress = getIpAddress();
		GeoInfo geoInfo = geoIpService.getGeoInfoByIp(ipAddress);
		if (geoInfo != null) {
			LOGGER.info("Found geoInfo and will try to populate the Reservation Controller instance with enriched data from the database.");
			setIpResolvedCountry(geoInfo.getCountryName());
			setIpResolvedCity(geoInfo.getCity());
			Country country = countryRepo.findCountryByCode(geoInfo.getCountryCode());
			if (country == null) {
				LOGGER.info("Unable to find country by 2-digit country code (" + geoInfo.getCountryCode() + ").");
				LOGGER.info("Attempting to find country by country name (" + geoInfo.getCountryName() + ").");
				country = countryRepo.findCountryByCountryName(geoInfo.getCountryName());
				if (country == null) {
					LOGGER.warn("Failed to find country by 2-digit code and country name");
					setDoPolling(false);
					return;
				}
			} else {
				// Set the ip resolved country name and city.
				updateFieldsWithGeoData(geoInfo, country);
			}
		} else {
			setDoPolling(false);
		}
	}

	private void updateFieldsWithGeoData(GeoInfo geoInfo, Country country) {
		LOGGER.info("Setting city (" + geoInfo.getCity() + "), country code (" + geoInfo.getCountryCode()
				+ ") and currency (" + country.getCurrencyCode() + ") information with ip info " + ipAddress);
		setCity(geoInfo.getCity());
		setCountryCode(country.getCountryCode());
		setPreferedCurrency(country.getCurrencyCode());
		setDoPolling(false);
	}

	public int getNumberOfDays(Reservation reservation) {
		if (reservation != null)
			return priceModelService.getDurationInDays(reservation.getStartDate(), reservation.getEndDate());
		else {
			LOGGER.warn("Requested to get number of days of null-value reservation object.");
			return -1;
		}
	}


	public List<Reservation> getOngoingReservations() {
		return ongoingReservations;
	}

	public void setOngoingReservations(List<Reservation> ongoingReservations) {
		this.ongoingReservations = ongoingReservations;
	}

	public Long getExistingReservationId() {
		return existingReservationId;
	}

	public void setExistingReservationId(Long existingReservationId) {
		this.existingReservationId = existingReservationId;
	}
}
